import { task } from 'gulp';
import { walkDirAndFilter, getBaseNameWithoutExt } from '../../util';
import * as path from 'path';
import * as fs from 'fs';
import * as ts from 'typescript';
import { srcPath, autoPath } from '../../config';

const API_FILE_TYPES = ['component', 'directive', 'service']

type ApiFileType = typeof API_FILE_TYPES[number];

class ApiItem {
  id: string;
  type: string;
  default: any;
  optional: boolean;
  comment: string;
  decoratorType?: string | undefined;
  params?: string[];
  returnType?: string;
  accessor?: string;
}

class ApiFileMap {
  className: string;
  type: ApiFileType;
  selector: string;
  filePath: string;
  apis: ApiItem[] = [];

  constructor(className: string, type: ApiFileType, selector: string, filePath: string) {
    this.className = className;
    this.type = type;
    this.selector = selector;
    this.filePath = filePath;
  }
}

class ApiComponentMap {
  [key: string]: ApiFileMap[];
}

task('genAPI', (done) => {
  const apiMap: ApiComponentMap = {};
  const filePaths: string[] = [];
  const supportTypes: ApiFileType[] = API_FILE_TYPES;

  // walk through all files
  walkDirAndFilter(path.join(srcPath, './fui'), '.md', (dir: any) => {
    const componentName = getBaseNameWithoutExt(dir.path);
    const dirName = path.dirname(dir.path);
    const apiArray: ApiFileMap[] = [];
    walkDirAndFilter(dirName, '.ts', (file: any) => {
      const filePath = file.path;
      const fileName = path.basename(filePath);

      filePaths.push(filePath);
      const fileApiArray = parseSourceFile(fileName, filePath);
      apiArray.push(...fileApiArray);
    });
    apiMap[componentName] = apiArray;
  });

  // create program & parse apis
  const program = ts.createProgram(filePaths, {});
  const checker: ts.TypeChecker = program.getTypeChecker();
  Object.keys(apiMap).forEach((componentName) => {
    apiMap[componentName].forEach((api) => {
      const sourceFile = program.getSourceFile(api.filePath);
      if (sourceFile) {
        api.apis = parseSourceFileApis(sourceFile, checker, api.className, api.type);
      }
    });

    apiMap[componentName].sort((apiA, apiB) => {
      const apiAIndex = supportTypes.indexOf(apiA.type);
      const apiBIndex = supportTypes.indexOf(apiB.type);
      return apiAIndex - apiBIndex;
    });
  });

  fs.writeFileSync(
    path.join(autoPath, 'api.ts'), `export default ${JSON.stringify(apiMap, null, 2)}`,
  ),
  done();
});

function parseSourceFile(fileName: string, filePath: string) {
  // Skip test files
  if (fileName.indexOf('spec') > -1) {
    return [];
  }

  // Skip unsupported file type
  const supportType = API_FILE_TYPES.find((type) => {
    if (fileName.indexOf(type) !== -1) {
      return type;
    }
  });
  if (!supportType) {
    return [];
  }

  const apiArray: ApiFileMap[] = [];
  const sourceFile = ts.createSourceFile(
    fileName,
    fs.readFileSync(filePath, 'utf8'),
    ts.ScriptTarget.Latest,
  );
  // Find each class declarations with decorator
  sourceFile.forEachChild(child => {
    if (ts.isClassDeclaration(child)) {
      const classDeclaration = child;
      const className = classDeclaration.name?.escapedText as string;
      let selector = '';

      if (!classDeclaration.decorators || !classDeclaration.decorators.length) {
        return;
      }
      const decorator = classDeclaration.decorators[0];
      const decoratorExpression = (decorator.expression as ts.PartiallyEmittedExpression).expression;
      const decoratorName = (decoratorExpression as ts.Identifier).escapedText;
      if (decoratorName === 'Component' || decoratorName === 'Directive') {
        const decoratorParams = (classDeclaration.decorators[0].expression as ts.CallExpression).arguments.reduce((acc, el) => {
          (el as ts.ObjectLiteralExpressionBase<any>).properties.forEach((prop) => acc[prop.name.escapedText] = prop.initializer.text);
          return acc;
        }, {});
        if (decoratorParams['selector']) {
          selector = decoratorParams['selector'];
        }
      }

      apiArray.push(new ApiFileMap(
        className,
        supportType,
        selector,
        filePath,
      ));
    }
  });

  return apiArray;
}

function parseSourceFileApis(sourceFile: ts.SourceFile, checker: ts.TypeChecker, className: string, type: ApiFileType) {
  const apis: ApiItem[] = [];
  const getterSetterSet = new Set<string>();

  const findClassAndVisit = (node: ts.Node | ts.SourceFile, className): void => {
    if (ts.isClassDeclaration(node)) {
      const nodeClassName = node.name?.escapedText as string;
      if (nodeClassName === className) {
        visit(node);
      }
    }

    ts.forEachChild(node, (childNode) => {
      findClassAndVisit(childNode, className);
    });
  };

  const visit = (node: ts.Node | ts.PropertyDeclaration | ts.AccessorDeclaration | ts.SourceFile): void => {
    if (ts.isPropertyDeclaration(node)) {
      parseNodeApi(node, type, checker, apis);
    }

    if (ts.isAccessor(node)) {
      const id = node.name.getText();
      if (!getterSetterSet.has(id)) {
        const api = parseNodeApi(node, type, checker, apis);
        if (api) {
          getterSetterSet.add(id);
        }
      }
    }

    if (type === 'service') {
      if (ts.isMethodDeclaration(node)) {
        parseNodeApi(node, type, checker, apis);
      }
    }

    ts.forEachChild(node, visit);
  };

  // visit(sourceFile);
  findClassAndVisit(sourceFile, className);

  return apis;
}

function parseNodeApi(
  node: ts.PropertyDeclaration | ts.AccessorDeclaration | ts.MethodDeclaration,
  type: ApiFileType,
  checker: ts.TypeChecker,
  apis: ApiItem[],
): ApiItem | null {
  const { name, decorators, questionToken } = node;
  let decoratorType: string | undefined;

  // For component and directive, only allow Input and Output
  if (type === 'component' || type === 'directive') {
    if (!decorators || !decorators.length) {
      return null;
    }

    const decorator = decorators[0];
    decoratorType = (decorator.expression as ts.PartiallyEmittedExpression).expression.getText();
    if (decoratorType !== 'Input' && decoratorType !== 'Output') {
      return null;
    }
  }

  // For service, only allow public properties and functions
  if (type === 'service') {
    const combinedFlag = ts.getCombinedModifierFlags(node);
    if ((combinedFlag & ts.ModifierFlags.Private) === ts.ModifierFlags.Private) {
      return null;
    }
  }

  const api: ApiItem = {
    decoratorType,
    id: name.getText(),
    type: '',
    default: null,
    optional: !!questionToken,
    comment: '',
  };

  if (ts.isAccessor(node)) {
    if (ts.isSetAccessor(node)) {
      api.accessor = 'set';
      const parameter = node.parameters[0];
      if (parameter && parameter.type) {
        api.type = parseTypeName(parameter.type);
      }
    }
    if (ts.isGetAccessor(node)) {
      api.accessor = 'get';
      api.type = parseTypeName(node.type);
    }
  }

  if (ts.isPropertyDeclaration(node)) {
    if (node.type) {
      api.type = parseTypeName(node.type);
    }
    if (node.initializer) {
      if (!api.type) {
        api.type = parseExpressionType(node.initializer);
      }
      api.default = node.initializer.getText();
    }
  }

  if (ts.isMethodDeclaration(node)) {
    api.type = 'function';
    api.params = node.parameters.map((parameter) => {
      return parameter.name.getText() + (parameter.questionToken ? '?' : '') +  ': ' + parseTypeName(parameter.type);
    });
    if (node.type) {
      api.returnType = parseTypeName(node.type);
    }
  }

  const symbol = checker.getSymbolAtLocation(node.name);
  if (symbol) {
    const comment = ts.displayPartsToString(symbol.getDocumentationComment(checker));
    if (comment) {
      api.comment = comment;
    }
  }

  apis.push(api);
  return api;
}

function parseTypeName(type: ts.TypeNode | undefined) {
  if (!type) {
    return '';
  }

  let typeName: string;
  switch(type.kind) {
    case ts.SyntaxKind.TypeLiteral:
      typeName = type.getText();
      break;
    case ts.SyntaxKind.TypeReference:
      typeName = (type as ts.TypeReferenceNode).typeName.getText();
      break;
    case ts.SyntaxKind.UnionType:
      typeName = (type as ts.UnionTypeNode).types.map((node) => node.getText()).join(' | ');
      break;
    case ts.SyntaxKind.ParenthesizedType:
      typeName = type.getText();
      break;
    case ts.SyntaxKind.FunctionType:
      const params = (type as ts.FunctionTypeNode).parameters.map((parameter) => {
        return parameter.name.getText() + (parameter.questionToken ? '?' : '') +  ': ' + parseTypeName(parameter.type);
      });
      const returnType = parseTypeName((type as ts.FunctionTypeNode).type);
      typeName = `(${params.join(', ')}) => ${returnType}`
      break;
    case ts.SyntaxKind.ArrayType:
      typeName = parseTypeName((type as ts.ArrayTypeNode).elementType) + '[]';
      break;
    case ts.SyntaxKind.BooleanKeyword:
      typeName = 'boolean';
      break;
    case ts.SyntaxKind.NumberKeyword:
      typeName = 'number';
      break;
    case ts.SyntaxKind.StringKeyword:
      typeName = 'string';
      break;
    case ts.SyntaxKind.AnyKeyword:
      typeName = 'any';
      break;
    default:
      typeName = ts.SyntaxKind[type.kind];
  }

  const typeArguments = (type as ts.NodeWithTypeArguments).typeArguments;
  if (typeArguments) {
    const typeArgumentNames = typeArguments.map((type) => parseTypeName(type));
    typeName = typeName + '<' + typeArgumentNames + '>';
  }

  return typeName;
}

function parseExpressionType(expression: ts.Expression) {
  switch(expression.kind) {
    case ts.SyntaxKind.NewExpression:
      return (expression as ts.PartiallyEmittedExpression).expression.getText();
    case ts.SyntaxKind.Identifier:
    case ts.SyntaxKind.PrefixUnaryExpression:
      return expression.getText().indexOf('Infinity') > -1 ? 'number' : expression.getText();
    case ts.SyntaxKind.FirstLiteralToken:
      return ts.isNumericLiteral(expression) ? 'number' : 'string';
    case ts.SyntaxKind.ArrayLiteralExpression:
      return 'array';
    case ts.SyntaxKind.TrueKeyword:
    case ts.SyntaxKind.FalseKeyword:
      return 'boolean';
    case ts.SyntaxKind.StringLiteral:
      return 'string';
    default:
      return ts.SyntaxKind[expression.kind];
  }
}
